﻿using Microsoft.Web.Administration;
using Microsoft.WindowsAzure;
using Microsoft.WindowsAzure.Diagnostics;
using Microsoft.WindowsAzure.Diagnostics.Management;
using Microsoft.WindowsAzure.ServiceRuntime;
using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Blob;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Security.AccessControl;
using System.Security.Cryptography.X509Certificates;
using System.Security.Principal;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using System.Xml;
using System.Xml.Serialization;
using Wams.Admin;

namespace Wams.Server
{
    public static class WamsCore
    {
        /// <summary>
        /// Maintains state for this site.
        /// </summary>
        private static WamsInstanceState _instanceState = null;
        public static WamsInstanceState InstanceState
        {
            get { return _instanceState; }
            set { _instanceState = value; }
        }

        public static CloudStorageAccount MainStorageAccount = null;
        public static string ContainerName = null;
        
        private static object _settingsSync = new object();
        public static WamsSettings Settings = null;

        internal static object _syncModules = new object();
        public static List<WamsServerModule> LoadedModules = new List<WamsServerModule>();

        internal static object _syncSites = new object();
        public static Dictionary<int, WamsServerSite> Sites = new Dictionary<int, WamsServerSite>();

        public static DiagnosticMonitorConfiguration DiagnosticConfig = null;
        private static bool _deferDiagnosticsUpdate;

        private static ManualResetEvent _recycleMre = new ManualResetEvent(false);


        #region Properties
        public static string InstallFolder
        {
            get { return RoleEnvironment.GetLocalResource("ResourcesRoot").RootPath.TrimEnd('\\'); }
        }

        internal static string RoleIP
        {
            get
            {
                IPHostEntry host = Dns.GetHostEntry(Dns.GetHostName());
                foreach (IPAddress ip in host.AddressList)
                {
                    if (ip.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork && ip != IPAddress.Loopback) return ip.ToString();
                }
                return null;
            }
        }
        #endregion

        #region Events
        public delegate void WamsEventDelegate();
        public static event WamsEventDelegate Init;
        public static event WamsEventDelegate Shutdown;
        #endregion

        public static void Initialise()
        {
            DateTime startTime = DateTime.Now;
            try
            {
                string storageConnString = CloudConfigurationManager.GetSetting("Wams.StorageConnectionString");
                if (storageConnString == null) throw new ApplicationException("Cloud configuration not set!");
                MainStorageAccount = CloudStorageAccount.Parse(storageConnString);
                if (MainStorageAccount == null) throw new ApplicationException("Could not set main storage account.");
            }
            catch { return; }

            ContainerName = CloudConfigurationManager.GetSetting("Wams.ContainerName");
            if (string.IsNullOrWhiteSpace(ContainerName)) ContainerName = "wams";
            else ContainerName = ContainerName.ToLower();
            
            Logger.Log(string.Format("Starting WAMS ({0}) {1}", ContainerName, Assembly.GetExecutingAssembly().GetName().Version.ToString(4)));
            Logger.Log(LogLevel.Info, string.Format("Install is set to {0}", WamsCore.InstallFolder));

            try
            {
                AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(LoadFromModuleFolders);

                int instanceId = int.Parse(RoleEnvironment.CurrentRoleInstance.Id.Substring(RoleEnvironment.CurrentRoleInstance.Id.LastIndexOf("_") + 1));
                InstanceState = new WamsInstanceState(RoleEnvironment.DeploymentId, instanceId, RoleEnvironment.CurrentRoleInstance.UpdateDomain);
                InstanceState.IsBusy = true;
                InstanceState.Status = InstanceStatus.Starting;
                SetServerWideIISSettings();

                LoadSettingsFile();
                _deferDiagnosticsUpdate = true;
                
                DeploymentDiagnosticManager diagManager = new DeploymentDiagnosticManager(
                    CloudConfigurationManager.GetSetting("Microsoft.WindowsAzure.Plugins.Diagnostics.ConnectionString"),
                    RoleEnvironment.DeploymentId);
                RoleInstanceDiagnosticManager instanceManager = diagManager.GetRoleInstanceDiagnosticManager(RoleEnvironment.CurrentRoleInstance.Role.Name, RoleEnvironment.CurrentRoleInstance.Id);
                DiagnosticConfig = instanceManager.GetCurrentConfiguration();


                LoadCertificates();
                LoadSites();
                LoadModules();

                foreach (WamsServerSite site in Sites.Values)
                {
                    site.SyncContent();
                    site.SyncSettings();
                }

                _deferDiagnosticsUpdate = false;
                UpdateDiagnostics();

                WamsCore.InstanceState.Status = InstanceStatus.Active;

                if (WamsCore.Init != null) WamsCore.Init();

                WamsCore.InstanceState.IsBusy = false;
                Logger.Log(LogLevel.Normal, string.Format("Startup successful ({0})!", DateTime.Now.Subtract(startTime)));
            }
            catch (Exception ex)
            {
                WamsCore.InstanceState.Status = InstanceStatus.Error;
                Logger.Log(LogLevel.Normal, string.Format("Ex while booting: {0}", ex.ToString()));
                throw;
            }
        }

        public static void UpdateDiagnostics()
        {
            //WamsCore.DiagnosticConfig.Directories.ScheduledTransferPeriod = TimeSpan.FromMinutes(1);
            if (_deferDiagnosticsUpdate) return;
            DiagnosticMonitor.Start("Microsoft.WindowsAzure.Plugins.Diagnostics.ConnectionString", WamsCore.DiagnosticConfig);
        }

        private static void SetServerWideIISSettings()
        {
            using (ServerManager serverManager = new ServerManager())
            {
                // Stops the site for the web role (it is not needed, and it interferes with admin site).
                if (serverManager.Sites.Count == 1 && serverManager.Sites[0].Name.EndsWith("_Web")) serverManager.Sites.RemoveAt(0);

                Configuration config = serverManager.GetApplicationHostConfiguration();
                ConfigurationSection httpProtocolSection = config.GetSection("system.webServer/httpProtocol");
                ConfigurationElementCollection customHeadersCollection = httpProtocolSection.GetCollection("customHeaders");

                ConfigurationElement addElement = customHeadersCollection.Where(x => x.Attributes["name"].Value.ToString() == @"X-Wams-Inst").SingleOrDefault();
                bool exists = false;
                if (addElement != null) exists = true;

                if (!exists)
                {
                    addElement = customHeadersCollection.CreateElement("add");
                    addElement["name"] = @"X-Wams-Inst";
                }

                addElement["value"] = RoleEnvironment.CurrentRoleInstance.Id;

                if (!exists) customHeadersCollection.Add(addElement);
                serverManager.CommitChanges();
            }
        }

        public static void LoadSettingsFile()
        {
            Logger.Log("Loading settings file...");
            try
            {
                lock (_settingsSync)
                {
                    InstanceState.Status = InstanceStatus.SettingsRefreshInProgress;
                    Settings = WamsSettings.Load(WamsCore.MainStorageAccount,WamsCore.ContainerName);
                    if (Settings == null) Settings = FirstTimeSetup.DoIt();
                    InstanceState.LastSettingsRefresh = DateTime.UtcNow;
                    InstanceState.Status = InstanceStatus.Active;
                }
            }
            catch (Exception ex)
            {
                InstanceState.Status = InstanceStatus.Error;
                Logger.Log(LogLevel.Error, "Ex while refreshing settings:\r\n" + ex.ToString());
            }
        }
        
        /// <summary>
        /// This method overrides the assembly resolution behaviour to allow custom modules to have their own modules.
        /// </summary>
        /// <param name="sender">The sender of the event.</param>
        /// <param name="args">The event arguments.</param>
        /// <returns>An assembly if found and loaded, or null if not found.</returns>
        private static Assembly LoadFromModuleFolders(object sender, ResolveEventArgs args)
        {
            // First check if the assembly is part of the Wams core.
            string folderPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
            string assemblyPath = Path.Combine(folderPath, new AssemblyName(args.Name).Name + ".dll");
            if (File.Exists(assemblyPath)) return Assembly.LoadFrom(assemblyPath);

            lock (_syncModules)
            {
                foreach (WamsServerModule module in WamsCore.LoadedModules)
                {
                    if (module.Content == null) continue;
                    try
                    {
                        assemblyPath = Path.Combine(module.Content.InstallPath, new AssemblyName(args.Name).Name + ".dll");
                        if (File.Exists(assemblyPath)) return Assembly.LoadFrom(assemblyPath);
                    }
                    catch { continue; }
                }
            }
            return null;            
        }

        private static void LoadModules()
        {
            lock (_syncModules)
            {
                LoadedModules.Clear();
                
                foreach (WamsModule item in Settings.Modules)
                {
                    WamsContent moduleContent = null;
                    if (!string.IsNullOrEmpty(item.Source))
                    {
                        moduleContent = new WamsContent(item.Source, null);
                        if (!moduleContent.Synchronise())
                        {
                            Logger.Log(LogLevel.Error, string.Format("Module {0} ({1}) could not be loaded: Content sync failed", item.Name, item.ClassName));
                            continue;
                        }
                    }
                    WamsServerModule moduleInstance = null;
                    if (string.IsNullOrEmpty(item.ClassName)) moduleInstance = new WamsServerModule();
                    else
                    {   // Custom module.
                        Type moduleType = Type.GetType(item.ClassName, false);
                        if (moduleType == null)
                        {
                            Logger.Log(LogLevel.Error, string.Format("Module {0} ({1}) could not be loaded: Type not found", item.Name, item.ClassName));
                            continue;
                        }
                        if (!moduleType.IsSubclassOf(typeof(WamsServerModule)))
                        {
                            Logger.Log(LogLevel.Error, string.Format("Module {0} ({1}) could not be loaded: Type does not inherit from WamsServerModule", item.Name, item.ClassName));
                            continue;
                        }
                        try
                        {
                            moduleInstance = Activator.CreateInstance(moduleType) as WamsServerModule;
                            if (moduleInstance == null) throw new ApplicationException("Module could not be initialised");
                        }
                        catch (Exception ex)
                        {
                            Logger.Log(LogLevel.Error, string.Format("Module {0} ({1}) could not be loaded: {2}", item.Name, item.ClassName, ex.ToString()));
                            continue;
                        }
                    }

                    moduleInstance.Content = moduleContent;
                    moduleInstance.Settings = item;
                    LoadedModules.Add(moduleInstance);
                }
            }

        }
        
        public static void LoadCertificates()
        {
            if (Settings.Certificates.Count == 0) return;
            int count = 0;
            
            Logger.Log("Refreshing certificates...");

            X509Store store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
            store.Open(OpenFlags.OpenExistingOnly | OpenFlags.ReadWrite);
                
            foreach (WamsCertificate certItem in Settings.Certificates)
            {
                try
                {
                    Logger.Log(string.Format("Processing certificate {0}.", certItem.FileName));
                    CloudBlobClient blobClient = WamsCore.MainStorageAccount.CreateCloudBlobClient();
                    CloudBlobContainer container = blobClient.GetContainerReference(WamsCore.ContainerName);
                    CloudBlockBlob certFileBlob = container.GetBlockBlobReference(string.Format("Certificates/{0}", certItem.FileName));
                    byte[] certFile;
                    using (MemoryStream ms = new MemoryStream())
                    {
                        certFileBlob.DownloadToStream(ms);
                        ms.Seek(0, SeekOrigin.Begin);
                        certFile = ms.ToArray();
                    }
                    Logger.Log(string.Format("Downloaded certificate {0} ({1} bytes).", certItem.FileName, certFile.Length));

                    X509Certificate2 certificate = new X509Certificate2(certFile, certItem.Password, X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet);

                    if (store.Certificates.Find(X509FindType.FindByThumbprint, certificate.Thumbprint, false).Count == 0)
                    {
                        Logger.Log(string.Format("Certificate not installed. Adding certificate {0}.", certItem.FileName));
                        store.Add(certificate);
                    }
                    else Logger.Log(string.Format("Certificate {0} already installed. Skipping.", certItem.FileName));
                    count++;
                }
                catch (Exception ex)
                {
                    Logger.Log(string.Format("Skipping certificate {0} due to ex:\r\n{1}",certItem.FileName,ex.ToString()));
                }
            }
            store.Close();
            Logger.Log(string.Format("{0} certificates processed.", count));
        }

        private static void LoadSites()
        {
            lock (_syncSites)
            {
                foreach (WamsSite item in Settings.Sites)
                {
                    WamsServerSite site;
                    if (Sites.Keys.Contains(item.Id)) site = Sites[item.Id];
                    else
                    {
                        site = new WamsServerSite() { Id = item.Id };
                        Sites.Add(site.Id, site);
                        site.State.Status = SiteStatus.Init;
                    }
                    site.Configure(item);                   
                }

                var activeSiteIds = Settings.Sites.Select(x => x.Id);
                foreach (int activeSiteId in Sites.Keys)
                {
                    if (!activeSiteIds.Contains(activeSiteId)) Sites[activeSiteId].Delete();
                }
            }
        }

        internal static void Stop()
        {
            Logger.Log(LogLevel.Warning, "Stopping instance " + RoleEnvironment.CurrentRoleInstance.Id);
            WamsCore.InstanceState.Status = InstanceStatus.ShuttingDown;

            // Call event for module shutdown.
            if (Shutdown != null) Shutdown();

            Parallel.ForEach(Sites.Values, site =>
            {
                site.State.Delete();
            });
            WamsCore.InstanceState.Delete();
        }

        public static void RequestRecycle()
        {
            Logger.Log(LogLevel.Warning, "Recycle requested.");
            _recycleMre.Set();
        }
        
        internal static void WaitToRecycle()
        {
            _recycleMre.Reset();
            _recycleMre.WaitOne();
            Logger.Log(LogLevel.Warning, "Recycling of instance started.");
            Stop();
            RoleEnvironment.RequestRecycle();
            throw new ApplicationException("Recycle requested.");

        }
    }
}